This document attempts to answer commonly asked questions about the document-handling classes in the AppKit. This includes the NSDocument class as well as NSDocumentController and NSWindowController.
Other sources of information about the document-handling classes can be found in several places:
To get started writing an NSDocument-based application, use the Document Based Application project template when creating an new ProjectBuilder project. When you do this, you will get a new application project which already contains a subclass of NSDocument, a document nib file, a template CustomInfo.plist to get you started defining your document types, and a Credits.rtf file for the standard About panel.
The NSDocument subclass is pre-set to load the document nib. Empty methods are provided for loading and saving, for you to fill in, and a method is provided for you to add code that will be called when the document nib is loaded.
Without doing anything, you should be able to compile and run the application. You will see an untitled document with an empty window get created when you first launch the application, and the File menu commands should all sort of work. But since you have not yet defined any types or implemented loading and saving, you won't actually succeed in opening or saving anything.
From here, you should start by defining a document type (see How do I define types?) and by implementing the load and save methods (see How do I implement saving and loading for simple files?).
Beyond defining types and implementing loading and saving, you will certainly have other stuff that needs to be implemented in your document subclass. The document subclass should contain and own the contents of the document. This means that your NSDocument subclass will want to provide API for maintaining and managing the document contents (ie the document's model objects).
For more information on this see the AppDesign documentation in the Programming Topics sections of the Tasks And Concepts manual. For an example of an application that follows the AppDesign document's design suggestions, see the Sketch example.
Types are currently defined in a file called CustomInfo.plist. This file should be part of the Supporting Files for your project if you created your project from the Document Based Application project template. If not, you can add a file with that name yourself.
The CustomInfo.plist is a dictionary with any property list contents you want, but for the purpose of defining types, there is one key which is important: the CFBundleDocumentTypes key. The value of the CFBundleDocumentTypes key should be an array of dictionaries which define the types your application understands. The default CustomInfo.plist looks like this:
{
CFBundleInfoDictionaryVersion = "5.0";
CFBundleVersion = "1.0.0d1";
CFBundleShortVersionString = "Version 1.0.0d1";
CFBundleName = "MyApp";
CFBundleIconFile = "MyAppIcon";
CFBundleDevelopmentRegion = "English";
CFBundlePackageType = "APPL";
CFBundleSignature = "Myap";
CFBundleGetInfoString = "MyApp version 1.0.0d1, Copyright (c) 19xx, Great Software, Inc.";
NSHumanReadableCopyright = "Copyright (c) 19xx, Great Software, Inc.";
CFBundleDocumentTypes = (
{
CFBundleTypeName = "DocumentType";
CFBundleTypeIconFile = "MyDocIcon"
CFBundleTypeExtensions = ("xxxx");
CFBundleTypeOSTypes = ("xxxx");
CFBundleTypeRole = Editor;
NSDocumentClass = Document;
}
);
}
Notice that the CFBundleDocumentTypes array contains one type dictionary. This dictionary defines a "dummy" type that you should alter. For a new application, you should at least change the name and extensions of the dummy type to values that make sense for your application. You can add more types as well. See the section on "Types and loading and saving" below for more details.
The application's "main" or most important document type should be listed first in the CFBundleDocumentTypes array.
The example CustomInfo.plist shown above also contains all the other standard keys that an application should typically define.
A new Document Based Application project comes with empty method implementations for -dataRepresentationOfType: and -loadDataRepresentation:ofType:. These methods should be implemented to support reading and writing of simple files.
You should implement -dataRepresentationOfType: to provide the contents of the document in the form of an NSData object in the requested type.
You should implement -loadDataRepresentation:ofType: to read in the document contents from the given NSData, interpreting the data as the given type.
See the Sketch application source for an example of how one app implements these methods.
If your document saves document as file wrappers or has other more sophisticated needs see How do I implement document packages (documents that are really folders)? and/or How do I implement loading and saving when the simple data or file wrapper API won't do? for more info.
The default Document Based Application project template does not subclass NSWindowController. Simple apps probably should not subclass NSWindowController, but apps with more advanced requirements almost certainly will end up wanting to subclass NSWindowController. Here are some common situations that would make subclassing desirable:
Subclassing NSWindowController is worthwhile in all but the simplest cases.
Once you've decided to subclass NSWindowController, a couple changes should be made to the default Document Based Application setup. First, any IB outlets and actions for your document's user interface that should be added to the NSWindowController subclass instead of the NSDocument subclass. This is because now, the NSWindowController subclass will be the nib file's owner. Some menu actions can still be implemented on the NSDocument subclass (save and revert are implemented by NSDocument for example, and you might add other menu actions of your own such as an action for creating new views on a document).
Second, instead of overriding -windowNibName in your NSDocument subclass, override -makeWindowControllers instead. In -makeWindowControllers you should alloc/init at least one NSWindowController (of your new subclass(s)) and use -addWindowController: to add it to the document. If your document always needs multiple controllers, create them all here. If your document will support multiple views, but by default has one, create the controller for the default view here and provide other user actions for creating other views.
Your override of -makeWindowControllers should not force the windows to be visible. NSDocument will do that for you if it should.
See the NSWindowControllers section below for more info.
A lot of the time, there are things that must be done right before or after a document's UI loads.
If you do not subclass NSWindowController then you can override NSDocument's -windowControllerWillLoadNib: or -windowControllerDidLoadNib: methods to do any extra setup you need to do.
If you do subclass NSWindowController then you should instead override NSWindowController's -windowWillLoad and -windowDidLoad methods.
Usually, an application that wants its documents to be document packages will use the NSFileWrapper class to construct and access its documents. If this is true for some or all of the types that your document class supports, then instead of overriding -dataRepresentationOfType: and -loadDataRepresentation:ofType: you should override -fileWrapperRepresentationOfType: and -loadFileWrapperRepresentation:ofType: instead.
These methods are much the same as the "data" methods, but they use NSFileWrappers instead.
If, for some reason, neither the NSData nor the NSFileWrapper loading and saving API will work for you, you can override the four methods that load and save document from file paths or URLs. These methods are:
- (BOOL)writeToFile:(NSString *)fileName ofType:(NSString *)type;
- (BOOL)writeToURL:(NSURL *)url ofType:(NSString *)type;
- (BOOL)readFromFile:(NSString *)fileName ofType:(NSString *)type;
- (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)type;
You should override all four of them if you are going to use these as your primitives for reading and writing. It is OK if you implement your file methods to call your URL methods or vice-versa.
These methods can also be overridden sometimes, not to provide the loading and saving behavior, but rather to do a little something right before or after calling super. This type of overriding is discussed below in How can I support reading a type and (internally) automatically converting to another? and How can I support stationery files?.
If your application has some types that it can read but not write, you can declare this by setting the CFBundleTypeRole key in the type dictionary for those types to be "Viewer" instead of "Editor". If you open a new document from a type that you are only a "Viewer" for it will come up as an untitled document, by default, because it cannot be written back out as the same file it was opened from.
If your application has some types that it can write, but not read, you can declare this by using the NSExportableAs key. You can include the NSExportableAs key in the type dictionary for another type that your document class supports. Usually this key would go in the type dictionary for the most "native" type for your document class. Its value is an array of type names that your document class can write, but not read.
The Sketch example uses this key to allow it to export TIFF and EPS images even though it cannot read those types.
Write-only types can only be chosen when doing "Save To...". They are not allowed for "Save" and "Save As..." operations.
Sometimes you might understand how to read a type, but not how to write it, and yet, when you read documents of that type, you want to automatically convert them to another type that you can write. An example of this would be an application that can read documents from an older version (or from a competing product). It might want to read in the old documents and automatically convert them to the documents new native format.
The first step is to add the old type as a read-only type (see How can I support read-only types?). By doing this, you will be able to open the old files, but they will come up as untitled files.
If you want to automatically convert them to be saved as your new type, you can override the -readFrom... methods in your NSDocument subclass to call super and then reset the file name and type afterwards. You should use -setFileType: and -setFileName: to set an appropriate type and name for the new document. For the file name you might want to make sure to strip the file extension of the old type from the original file name, if it is there, and add the extension for the new type (if necessary).
If you support stationery documents, when someone opens a stationery document you usually want it to come up as an untitled file. Most apps that support stationery have a special document type for stationery. Because you usually want to allow the user to save a document as stationery, your application will support the stationery type with an "Editor" NSRole.
But, since you want the stationery behavior of having stationery documents open as untitled, you should override the -readFrom... methods to call super and then set the file name of the document to be nil and set the file type of the document to be whatever your default native document type is.
Sometimes a document that is stored as a document package might have very large pieces inside it that are not often needed by the application and which the app therefore loads lazily, if at all. When such documents are saved, it would be unfortunate to have to read in the large pieces and write them back out in the new location every time.
NSDocument uses a back-up strategy while saving. So, if the document being saved already exists at the location where it is saving, it is moved aside first to a back-up location before the document is saved. Then, if -keepsBackupFile is NO the back-up is removed after then new file is saved.
If your documents have potentially large pieces and you want to optimize the save operation to avoid unnecessary loading and saving of those large pieces, you can override -writeToFile:ofType:originalFile:saveOperation: to accomplish it. This method is called by the save mechanism and, by default it just calls -writeToFile:ofType:. But if you need to know where the original copy of the document is (if there is one), then this method provides that info.
To implement this optimization properly you should remember that it is only safe to actual move the large pieces from the old location to the new if all the following are true:
If you cannot do the move, you might still be able to optimize by making a copy through the file system rather than loading and saving the large pieces into your app if there is an original file.
You can control whether the default accessory view (which contains a popup allowing the user to choose what type to save) is used by overriding -shouldRunSavePanelWithAccessoryView. The default accessory view will be used if that method returns YES and the document supports writing multiple types.
You can get a chance to customize the panel more completely by overriding -runModalSavePanel:withAccessoryView: and futzing with the panel a bit before calling super.
You could also conceivably replace the accessory view.
Subclasses of NSDocument that wish to support printing should override -printShowingPrintPanel:. Usually this is implemented to create an NSPrintOperation with the document's print info and run it.
A document ought to be prepared to print itself even if it currently has no window controllers.
Ideally you should treat a document's print info as part of the document, to be saved and loaded along with the rest of the contents. This is not always possible if your document format is already defined and is not flexible enough to allow saving the print info.
Apart from using it to create your print operations and possibly saving and loading it, you should not have to do anything else about print info.
NSWindowController itself expects to be told by someone else what nib file to load (through its -initWithNib... methods). But when you write a subclass of NSWindowController it is almost always meant for a specific nib file that it implements the UI behavior for. It is inconvenient and error-prone for the client of the subclass to have to tell you which nib file to load.
This is easily solved by overriding the -init method to simply call super's -initWithNibName: with the correct nib name. Now clients just use -init and the controller has the correct nib.
If you want, you can also override the -initWithNib... methods to log an error, since no clients should ever try to tell your subclass which nib file to use.
Doing this is a good idea for almost all NSWindowController subclasses.
An NSWindowController without an associated NSDocument is a useful thing all by itself. NSWindowController can be used as the base class for auxiliary panel controllers in order to gain the use of its nib management abilities.
One common stand-alone use of NSWindowController subclasses is as controllers for shared panels such as find panels, inspectors, or preferences panels. In this case you can make an NSWindowController subclass which implements a shared instance method. For instance, you could create a PreferencesController subclass with a +sharedPreferenceController method which would create a single instance the first time it is called and return that same instance on all subsequent calls.
Because it is an NSWindowController, you can just tell it the name of your Preferences nib and it will handle all the loading and management of the window. You add your own outlets and actions, as usual, to hook up the specific UI for your panel, and add the API to manage the panels behavior.
The Sketch application uses NSWindowController subclasses for its various secondary panels.
NSWindowController can even be put to good use when you don't need a window. Sometimes, you want a controller for a piece of UI that might be swapped in and out of a window or put in various kinds of places depending on the context. But that UI still usually comes from a nib file.
NSWindowController can be used to manage the loading of that UI and the management of it. In this case, you will want to create outlets and actions and behavioral logic for all the UI as you normally would in an NSWindowController subclass. But you also override -windowDidLoad to remove the UI you actually want from the window it must be in for the nib (remember to retain your top-level view before removing it from the view hierarchy so it won't be deallocated). Finally, once the actual UI has been extracted from the window, you can use -setWindow:nil to get rid of the unneeded window.
The NSWindowController will still realize that the nib file is loaded, even though the window has gone away, so it will not attempt to load it again. The result of all this is that you have a controller for a view or set of views, that can be swapped in and out of different windows as needed.
If your NSWindowController has no window and it is attached to a document, then there is no where for the document to display its dirty state. But NSDocument always keeps its window controllers informed of its dirty state with -setDocumentEdited: so a windowless NSWindowController can implement other ways of indicating the dirty state, if necessary.
In some applications you want to have a whole bunch of open documents, but only be showing one (or at least not all of them). Think of a mail client that treats each message as a document. There is a mailbox window with an area to display a message, but as you go from message to message, they get displayed in the same window.
This is a case where you want an NSWindowController that is swapped around among the open documents. Sometimes the controller belongs to and is showing one document, and sometimes another.
To implement this you can use -addWindowController: and -removeWindowController: to move the controller from document to document. In fact if the window controller passed to -addWindowController: is owned by another document already, this call will remove it from the old document first.
You may also want to override -makeWindowControllers in your NSDocument subclass to either not create any controllers or possibly to find the one shared controller and take it over.
If you use -makeWindowControllers to create your window controllers (see How do I subclass NSWindowController?), you can create more than one, possibly of different subclasses of NSWindowController, from the beginning. Another possibility is to allow the user to create new controllers later as they need them. In any event, multiple controllers can be added to a document with -addWindowController:.
By default, a document will close when its last remaining window controller closes. Specific window controllers can also be set to close the document when they close even if there are other controllers still open. An example of where this might happen is Interface Builder. There is a main window for a nib document (the one with the tab view and the top-level instances in it), and there are possibly a bunch of other windows (the window editors for the actual windows in the nib). If the user closes the main window, all the rest close as well and the document itself closes. If IB used the document architecture, it would use -setShouldCloseDocument:YES one the main window's controller to implement this behavior.
If desired, you can override the NSWindowController -windowTitleForDocumentDisplayName: method to modify the the title for each view. For instance, a CAD program might have the titles of the different windows it uses for the document "Airplane" as "Airplane - Top", "Airplane - Side", etc...
Undo is not always an easy thing to implement. But at least the mechanism for implementing it is really simple. By default, an NSDocument has its own NSUndoManager. NSUndoManager works by allowing you to easily construct invocations that will do the opposite of what you do when a change happens.
The key is to have well-defined primitives for changing your document. Each model object and the document subclass itself should define the primitive sets of methods that can change them. Each of these primitives is then responsible for using the undo manager to enqueue invocations to undo those primitives. For example, if you decide that -setColor: is a primitive for one of your model objects, then inside of -setColor: your object would do something like: [[[myDocument undoManager] prepareInvocationWithTarget:self] setColor:oldColor]. This call will cause an NSInvocation to be constructed and saved away. If the user later chooses Undo, that invocation will be invoked and your model object will receive another -setColor: message, this time with the old color. (If you're wondering if you have to keep track of whether things are being undone and avoid doing the undo manager stuff again, you don't. In fact, the way that Redo works is by watching what invocations get registered as the Undo is happening and recording them on the Redo stack!)
Another piece of good undo implementation is to provide action names so that the Undo/Redo menu items can have more descriptive titles. Undo action names are usually best set in action methods instead of the change primitives in your model objects because many primitive changes might go into one user action or different user action might result in basically the same primitives being called in different ways.
Because NSUndoManager does multiple level undo, it is not good to have only some change undo-able. The undo manager relies on being able to reliably take the document back through history with repeated undos. The problem is that if some changes get skipped, the undo stack state will no longer in synch with the contents of the document. Depending on your architecture that can cause problems that range from the merely annoying to the fatal.
If there are some changes that you just can't undo, there are a couple possibilities. If you can be absolutely sure that the changes have no relationship to any other changes that can happen to the document (ie something totally independent of all the rest of contents of the document has changed), then you can safely just not register any undo action. But, if the change does have some relationship to the rest of the document contents, the safe thing to do is remove all actions from the undo manager when such a change takes place. Such changes then mark points of no return in your user experience.
This sort of thing really ought to be avoided whenever possible.
If you don't wish to support undo at all, the first thing you should do is call -setHasUndoManager:NO on your documents. This will cause the document never to get an undo manager.
Without an undo manager (and undo support from your model objects), the document cannot automatically track its dirty state. So, if you aren't implementing Undo, you will need to call -updateChangeCount: by hand when your document is edited.
Because of Undo support, the document has to keep more information than just whether the document is dirty or clean. If I open a file, make five changes, and then hit Undo five times, the document should once again be clean. But if I hit Undo only four times, the document is still dirty.
NSDocument keeps a change count to deal with this. The change count can be modified by calling -updateChangeCount: with one of the supported change types. The supported changes are NSChangeDone, NSChangeUndone, and NSChangeCleared. NSDocument itself clears the change count whenever someone saves or reverts the document. If the document has an undo manager, it observes the undo manager and automatically updates the change count when changes are done, undone, or redone.
If your document subclass does not support Undo, then you will need to inform NSDocument of edits with -updateChangeCount: yourself (see What if I don't want to support undo?).
Usually, it should not be necessary to subclass NSDocumentController. Almost anything that can be done by subclassing can be done just as easily by the application's delegate. However, it is possible should you find yourself needing to.
There are two reasonable ways to subclass NSDocumentController.
You can make an instance of your subclass in your application's main nib file. This instance will become the shared instance.
Or you can create an instance of your subclass in your application's delegate's -applicationWillFinishLaunching: method.
The first NSDocumentController to be alloc/init'd will become the shared instance. The AppKit itself will create the shared instance (using the NSDocumentController class) during the "finish launching" phase of app startup. So if you need a subclass you must create it before the kit does.
You can use NSDocumentController's -open... methods, which will create the documents and, is -shouldCreateUI is YES will also create the window controller(s) and will add the document to the list of open documents. These methods will also unique on file paths and return an existing instance if it exists.
You can use NSDocumentController's -make... methods, which will just create the document. Usually, you will at least want to call -addDocument: to add the new document to the NSDocumentController as well.
You can simply alloc/init one with any initializer the subclass supports. Usually, you will at least want to call NSDocumentController's -addDocument: to add the new document to the NSDocumentController as well.
Some apps support multiple types of document that are really separate (rather than a single document that might be stored in any number of types). In these apps, opening existing documents is not usually a problem, since what kind of document to create is determined from the file being opened. But creating new documents in such an app is trickier.
NSDocumentController's -newDocument: action will create a new document of the first type listed in the app's NSTypes. But this isn't really enough for such apps.
Instead you can create your own new action(s), either in your application's delegate or in an NSDocumentController subclass. You could create several action methods and have several different New menu items or you could have one action which would somehow ask the user to pick a document type before creating a new one.
Your new method can simply use the -openUntitledDocumentOfType: method in NSDocumentController to actually create a document of the correct type.
If you need to customize the Open panel, that is one of the clear times when an NSDocumentController subclass is needed. You can override the -runModalOpenPanel:forTypes: method to customize the panel or add an accessory view.